package com.platform.module.common.redis.lock;

import com.platform.module.common.base.CommonResp;
import com.platform.module.common.exception.CommonException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;

import static com.platform.module.common.redis.DistributedLockAspectConfiguration.lockTL;


public class RedisDistributedLock extends AbstractDistributedLock {
    private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);

    private final RedisTemplate<String, Object> redisTemplate;

    private final ThreadLocal<String> lockFlag = new ThreadLocal<>();

    private static final String DEFAULT_VAL = "default";

    private static final DefaultRedisScript<Boolean> redisScript;

    static {
        redisScript = new DefaultRedisScript<>();
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) return true else return false end";
        redisScript.setScriptText(script);
        redisScript.setResultType(Boolean.class);
    }

    public RedisDistributedLock(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean lock(String key, Runnable runnable) {
        key = LOCK_PREFIX + key;
        boolean lock = lock(key, 30000L, 2147483647, 500L);
        if (!lock) {
            this.logger.warn("获取锁失败！key={}", key);
            return false;
        }
        try {
            runnable.run();
            return true;
        } catch (Exception e) {
            this.logger.error("execute locked method occurred an exception", e);
            return false;
        } finally {
            boolean releaseResult = this.releaseLock(key);
            this.logger.debug("release lock : " + key + (releaseResult ? " success" : " failed"));
        }
    }

    @Override
    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, expire);
        while (!result && retryTimes-- > 0) {
            try {
                this.logger.debug("lock failed, retrying..." + retryTimes);
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                return false;
            }
            result = setRedis(key, expire);
        }
        return result;
    }


    private boolean setRedis(String key, long expire) {
        RedisLock redisLock = lockTL.get();
        try {
            String value = "default";
            if (redisLock != null && !RedisLock.UnLockMode.TIMEOUT.equals(redisLock.unlockMode())) {
                value = UUID.randomUUID().toString();
                this.lockFlag.set(value);
            }
            Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofMillis(expire));
            return Boolean.TRUE.equals(success);
        } catch (Exception e) {
            this.logger.error("set redis occurred an exception", e);
        } finally {
            lockTL.remove();
        }
        return false;
    }

    @Override
    public boolean releaseLock(String key) {
        try {
            String arg = this.lockFlag.get();
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("release lock key:" + key + ",arg:" + arg);
            }
            Boolean result = redisTemplate.execute(redisScript, List.of(key), arg);
            return result != null && result;
        } catch (Exception e) {
            this.logger.error("release lock occurred an exception", e);
        } finally {
            this.lockFlag.remove();
        }
        return false;
    }

    @Override
    public boolean lock(String key, String uuid, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, uuid, expire);
        while (!result && retryTimes-- > 0) {
            try {
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                return false;
            }
            result = setRedis(key, uuid, expire);
        }
        return result;
    }

    private boolean setRedis(String key, String uuid, long expire) {
        try {
            Boolean success = redisTemplate.opsForValue().setIfAbsent(key, uuid, Duration.ofMillis(expire));
            return Boolean.TRUE.equals(success);
        } catch (Exception e) {
            this.logger.error("set redis occurred an exception", e);
        }
        return false;
    }


    @Override
    public boolean releaseLock(String key, String uuid) {
        try {
            Boolean result = redisTemplate.execute(redisScript, List.of(key), uuid);
            return result != null && result;
        } catch (Exception e) {
            this.logger.error("release lock occurred an exception", e);
        }
        return false;
    }

    @Override
    public <T> CommonResp<T> lock(String key, long expire, int retryTimes, long sleepMillis, Supplier<CommonResp<T>> supplier) {
        key = LOCK_PREFIX + key;
        boolean lock = this.lock(key, expire, retryTimes, sleepMillis);
        if (!lock) {
            this.logger.warn("手速太快了，请稍微休息会儿！key={}", key);
        }
        if (logger.isDebugEnabled()) {
            this.logger.debug("get lock success : " + key);
        }
        try {
            return supplier.get();
        } catch (Exception e) {
            this.logger.error("execute locked method occurred an exception", e);
            if (e instanceof CommonException) {
                return CommonResp.fail(((CommonException) e).code(), e.getMessage());
            }
            return CommonResp.fail();
        } finally {
            boolean releaseResult = this.releaseLock(key);
            this.logger.debug("release lock : " + key + (releaseResult ? " success" : " failed"));
        }
    }


}
